package com.liuwei.msmall.seckill.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.google.common.collect.Lists;
import com.liuwei.msmall.common.dto.MemberRespDTO;
import com.liuwei.msmall.common.dto.mq.SeckillOrderTo;
import com.liuwei.msmall.common.utils.DateUtils;
import com.liuwei.msmall.common.utils.R;
import com.liuwei.msmall.seckill.fegin.MarketingFeignService;
import com.liuwei.msmall.seckill.fegin.ProductFeignService;
import com.liuwei.msmall.seckill.interceptor.LoginInterceptor;
import com.liuwei.msmall.seckill.service.SeckillService;
import com.liuwei.msmall.seckill.to.SeckillSkuRedisTo;
import com.liuwei.msmall.seckill.vo.SeckillSessionWithSkusVo;
import com.liuwei.msmall.seckill.vo.SeckillSkuVo;
import com.liuwei.msmall.seckill.vo.SkuInfoVo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.time.ZoneId;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * @Classname SeckillServiceImpl
 * @Date 2021/5/5 2:14
 * @Created by 刘
 */
@Slf4j
@Service
public class SeckillServiceImpl implements SeckillService {

    @Autowired
    MarketingFeignService marketingFeignService;

    @Autowired
    ProductFeignService productFeignService;

    @Autowired
    StringRedisTemplate redisTemplate;

    @Autowired
    RabbitTemplate rabbitTemplate;

    @Autowired
    RedissonClient redissonClient;

    //缓存秒杀活动场次的key前缀
    private final String SESSIONS_CACHE_PREFIX = "seckill:sessions:";//后面接秒杀活动的开始和结束时间的组合

    //缓存商品详情的key前缀
    private final String SECKILLSKU_CACHE_PREFIX = "seckill:skus";

    //商品秒杀对应的库存信号量key前缀semaphore
    private final String SKU_STOCK_SEMAPHORE = "seckill:stock:";//后面接商品对应的随机码

    @Override
    public void uploadSkuLatest3Days() {

        //远程调用优惠营销系统查询最近3天的秒杀活动场次
        R r = marketingFeignService.getLatest3DaysSession();
        if (r.getCode() == 0) {
            List<SeckillSessionWithSkusVo> data = r.getData("data", new TypeReference<List<SeckillSessionWithSkusVo>>() {
            });
            log.info("需要上架的秒杀信息：{}", JSON.toJSON(data));
            if (CollectionUtils.isEmpty(data)) {
                return;
            }
            //1.缓存活动场次信息
            saveSeckillSessionInfo(data);
            //2.缓存商品详情信息
            saveSeckillSkuInfo(data);
        }

    }

    @Override
    public List<SeckillSkuRedisTo> getCurrentSeckillSkuInfo() {

        long curTime = new Date().getTime();
        //获取秒杀场次对应的key集合 seckill:sessions:1603382400000_1603386000000
        Set<String> sessionsKeys = redisTemplate.keys(SESSIONS_CACHE_PREFIX + "*");
        if (sessionsKeys != null && sessionsKeys.size() > 0) {
            for (String key : sessionsKeys) {
                //seckill:sessions:1603382400000_1603386000000
                String replace = key.replace(SESSIONS_CACHE_PREFIX, "");
                String[] split = replace.split("_");
                //1.获取秒杀活动场次的秒杀时间区间
                long startTime = Long.parseLong(split[0]);
                long endTime = Long.parseLong(split[1]);
                //判断当前时间是否属于该场次
                if (curTime >= startTime && curTime <= endTime) {
                    //2.封装当前时间的秒杀商品信息
                    List<String> sessionId_skuIds = redisTemplate.opsForList().range(key, -100, 100);
                    BoundHashOperations<String, String, String> ops = redisTemplate.boundHashOps(SECKILLSKU_CACHE_PREFIX);
                    //通过key集合获取商品详细信息
                    List<String> list = ops.multiGet(sessionId_skuIds);
                    if (list != null && list.size() > 0) {
                        List<SeckillSkuRedisTo> collect = list.stream().map(item -> {
                            //封装成对应vo
                            SeckillSkuRedisTo redisTo = JSON.parseObject(item, SeckillSkuRedisTo.class);

                            //随机码的展示，随机码不能随便展示给前台，只有当秒杀活动已经开始了，才返回，否则不能返回
//                        redisTo.setRandomCode(null);
                            return redisTo;
                        }).collect(Collectors.toList());
                        return collect;
                    }

                    break;

                }

            }
        }
        return Lists.newArrayList();
    }

    @Override
    public SeckillSkuRedisTo getSkuSeckillInfoById(Long skuId) {
        BoundHashOperations<String, String, String> ops = redisTemplate.boundHashOps(SECKILLSKU_CACHE_PREFIX);
        //拿到缓存秒杀商品信息的所有key
        Set<String> keys = ops.keys();
        if (keys != null && keys.size() > 0) {
            //使用正则表达式匹配该商品对应的key
            String regx = "\\d_" + skuId;
            for (String key : keys) {
                if (Pattern.matches(regx, key)) {
                    //如果匹配成功
                    String json = ops.get(key);
                    SeckillSkuRedisTo redisTo = JSON.parseObject(json, SeckillSkuRedisTo.class);
                    //判断当前时间是否再秒杀时间内
                    long curTime = new Date().getTime();
                    if (curTime >= redisTo.getStartTime() && curTime <= redisTo.getEndTime()) {

                    } else {
                        //如果不是，就将商品对应的随机码进行置空
                        redisTo.setRandomCode(null);
                    }
                    return redisTo;
                }
            }
        }
        return null;
    }

    @Override
    public String kill(String key, Integer count, String killId) {
        long currentTime = System.currentTimeMillis();
        //拿到当前已经登录的用户
        MemberRespDTO memberRespVo = LoginInterceptor.loginUserLocal.get();

        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILLSKU_CACHE_PREFIX);
        //标识一位用户  userId_sessionId_skuId;
        String userFlagKey;
        String s = hashOps.get(killId);

        /**
         * 1.进行合法性校验
         */
        if (!StringUtils.isEmpty(s)) {
            SeckillSkuRedisTo redisTo = JSON.parseObject(s, SeckillSkuRedisTo.class);

            //判断秒杀时间是否合理
            if (currentTime >= redisTo.getStartTime() && currentTime <= redisTo.getEndTime()) {

                //判断随机码，场次id和skuid
                String killId2 = redisTo.getPromotionSessionId() + "_" + redisTo.getSkuId();
                if (redisTo.getRandomCode().equals(key) && killId2.equals(killId)) {

                    //验证每个人的抢购限制
                    if (count <= redisTo.getSeckillLimit()) {
                        //验证这个用户是否已经买过

                        userFlagKey = memberRespVo.getId() + "_" + redisTo.getPromotionSessionId() + "_" + redisTo.getSkuId();
                        //在redis中占位 SETNX ,判断key是否存在，如果存在则占位失败，如果key不存在则占位成功，这个过程是原子性的
                        Boolean absent = redisTemplate.opsForValue().setIfAbsent(userFlagKey, count.toString(), redisTo.getEndTime() - currentTime, TimeUnit.MILLISECONDS);
                        if (absent) {
                            //占位成功
                            //扣减库存信号量，发送mq消息
                            RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + key);

                            boolean acquire = semaphore.tryAcquire(count);
                            if (acquire) {
                                //2.扣减成功，生成订单号，发送mq消息
                                String orderSn = IdWorker.getTimeId();
                                SeckillOrderTo orderTo = new SeckillOrderTo();

                                orderTo.setNum(count);
                                orderTo.setSkuId(redisTo.getSkuId());
                                orderTo.setOrderSn(orderSn);
                                orderTo.setSeckillPrice(redisTo.getSeckillPrice());
                                orderTo.setMemberId(memberRespVo.getId().intValue());
                                try {
                                    rabbitTemplate.convertAndSend("order-event-exchange", "order.seckill.order", orderTo);
                                }catch (Exception e){
                                    log.error("发送订单创建消息失败:{},消息内容：{}",e.getMessage(),JSON.toJSON(orderTo));
                                }

                                log.info("秒杀耗时：{}",System.currentTimeMillis()-currentTime);
                                return orderSn;
                            }

                        }
                    }
                }
            }

        }
        return null;
    }

    private void saveSeckillSkuInfo(List<SeckillSessionWithSkusVo> data) {

        long currentTime = System.currentTimeMillis();
        //绑定hashkey
        BoundHashOperations<String, String, String> boundHashOperations = redisTemplate.boundHashOps(SECKILLSKU_CACHE_PREFIX);
        data.stream().forEach(session -> {

            //拿到该秒杀场次所关联的商品基本信息
            List<SeckillSkuVo> skuRelationEntities = session.getSkuRelationEntities();
            if (!CollectionUtils.isEmpty(skuRelationEntities)) {
                //遍历每个商品的秒杀信息
                skuRelationEntities.forEach(skuRelation -> {
                    //判断这个场次下所关联的商品有没有被上架过，如果已经上架过就不需要再上架了
                    if ((DateUtils.getTimestampOfDateTime(session.getEndTime()) - currentTime) > 0 && !boundHashOperations.hasKey(session.getId().toString() + "_" + skuRelation.getSkuId().toString())) {

                        log.info("缓存秒杀商品信息，库存信号量");

                        SeckillSkuRedisTo skuRedisTo = new SeckillSkuRedisTo();

                        //1.保存秒杀商品的基本信息
                        BeanUtils.copyProperties(skuRelation, skuRedisTo);

                        //2.保存商品的详细信息
                        //远程调用商品服务查询商品详细信息
                        R r = productFeignService.getSkuInfo(skuRelation.getSkuId());
                        if (r.getCode() == 0) {
                            SkuInfoVo skuInfo = r.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                            });
                            skuRedisTo.setSkuInfo(skuInfo);
                        }

                        //3.保存商品的秒杀时间
                        skuRedisTo.setStartTime(DateUtils.getTimestampOfDateTime(session.getStartTime()));

                        skuRedisTo.setEndTime(DateUtils.getTimestampOfDateTime(session.getEndTime()));

                        //4.保存商品的随机码:作用：防止接口在秒杀时间到之前前被而恶意人员刷
                        String token = UUID.randomUUID().toString().replace("-", "");
                        skuRedisTo.setRandomCode(token);

                        //5.使用秒杀商品的秒杀库存作为秒杀系统中分布式的信号量，作用：用于限流
                        //使用生成的随机码作为key
                        RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                        //设置该商品的随机码对应的信号量
                        semaphore.trySetPermits(skuRedisTo.getSeckillCount().intValue());
                        //todo 设置库存信号量过期时间
                        semaphore.expire(skuRedisTo.getEndTime() - currentTime, TimeUnit.MILLISECONDS);
                        //将保存的to转换为json字符串保存在redis中
                        String jsonString = JSON.toJSONString(skuRedisTo);
                        boundHashOperations.put(session.getId().toString() + "_" + skuRelation.getSkuId().toString(), jsonString);
                        //设置缓存过期时间
                        //todo 设置商品详情的key的过期时间
//                        boundHashOperations.expire(skuRedisTo.getEndTime()-currentTime,TimeUnit.MILLISECONDS);
                    }

                });
            }

        });

    }

    private void saveSeckillSessionInfo(List<SeckillSessionWithSkusVo> data) {

        data.forEach(session -> {
            //活动开始时间
            long startTime = session.getStartTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
            //结束时间
            long endTime = session.getEndTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
            //保存的key
            String key = SESSIONS_CACHE_PREFIX + startTime + "_" + endTime;
            //获取当前时间
            long currentTime = System.currentTimeMillis();
            /**
             * 保证幂等性,并且判断活动是否已经结束
             */
            if (!CollectionUtils.isEmpty(session.getSkuRelationEntities()) && (endTime - currentTime) > 0 && !redisTemplate.hasKey(key)) {
                log.info("缓存秒杀场次信息。。。。，key:{}", key);
                List<String> skuIds = session.getSkuRelationEntities().stream().map(item -> item.getPromotionSessionId().toString() + "_" + item.getSkuId().toString()).collect(Collectors.toList());
                //缓存到redis中
                redisTemplate.opsForList().leftPushAll(key, skuIds);
                //todo 设置秒杀场次key的过期时间

                redisTemplate.expire(key, endTime - currentTime, TimeUnit.MILLISECONDS);

            }

        });

    }

}
